package robocup.component;

import robocup.component.infotypes.PlayerParamInfo;

/**
 * The class PConf (Player-Configuarion) captures all player specific
 * parameters, which can occur in a game. This includes parameter for
 * heterogeneous player types. It implements the <code>Comparable</code>-Interface
 * in order to sort the configurations by different heterogeneous attributes,
 * e.g. dash-power of kiackable margin. This is important for the coach, which
 * assigns these types to agents.
 * <p>
 * Note that changing these values here does not affect the simulation itself,
 * but the way an agent thinks about the simulation. E.g. if an agent changes
 * his dash-power-rate here, he thinks that he dashes faster, but he doesn't,
 * because simulation is done by the server. Note that also the player types of
 * the opponent team is not known and hence always type 0, which is almost
 * always wrong (only goalies always have type 0), because the other types are
 * faster, have better dribbling skills (kickable margin) or turn faster
 * (inertia moment).
 */
public class PConf
    implements Comparable<PConf> {

  /**
   * This enumeration holds all heterogeneous attributes, which can be compared.
   */
  public enum COMPARE_TYPE {
    SPEED,
    INERTIA,
    KICKABLE_DISTANCE,
    DASH_POWER,
    KICKABLE_AND_DASH_POWER_AND_STAMINA,
    DASH_POWER_AND_STAMINA,
    KICKABLE_AND_INERTIA,
    DASH_POWER_AND_DOUBLE_STAMINA,
    DOUBLE_DASH_POWER_AND_STAMINA,
    STAMINA,
    STAMINA_AND_KICKABLE
  }

  /**
   * Field which says how to compare PConfs!
   */
  private COMPARE_TYPE    compareType       = COMPARE_TYPE.SPEED;

  /**
   * A player's max acceleration
   */
  public double           PLAYER_ACCEL_MAX  = 1.0;

  /**
   * A player's max speed
   */
  public double           PLAYER_SPEED_MAX  = 1.2;

  /**
   * A player's max stamina increase
   */
  public double           STAMINA_INC_MAX   = 45.0;

  /**
   * The rate of deceleration
   */
  public double           PLAYER_DECAY      = 0.4;

  /**
   * A player's intertia for turning turning_angel = moment /
   * (1+inertia_moment*speed)
   */
  public double           INERTIA_MOMENT    = 5.0;

  /**
   * Base power of a dash, effectiv dash power is effort*dash_power_rate*power
   */
  public double           DASH_POWER_RATE   = 0.006;

  /**
   * A player's radius.
   */
  public double           PLAYER_SIZE       = 0.3;

  /**
   * A player is able to kick the ball within this radius.
   */
  public double           KICKABLE_MARGIN   = 0.7;

  /**
   * Noise added to a kick
   */
  public double           KICK_RAND         = 0.0;

  /**
   * If there's not enough stamina for a dash, player will use a bit of extra
   * stamina
   */
  public double           EXTRA_STAMINA     = 0.0;

  /**
   * max player dash effort
   */
  public double           EFFORT_MAX        = 1.0;

  /**
   * min player dash effort
   */
  public double           EFFORT_MIN        = 0.6;

  /**
   * The id of the heterogeneous player type defined in and received from the
   * server. The default player type (hence also the goalie) is id 0. All
   * default values given above refer to type 0. The others (usually type 1 to
   * 6) have different strengths and weaknesses.
   */
  private int             id                = 0;

  /**
   * The effective max-speed is defined in getEffectiveMaxSpeed. For type 0,
   * this is 1, all others depend on the player types dash-power, its decay and
   * the max-effort. Nevertheless, no player can run faster than
   * player-max-speed, which is 1.2 in the usual setting.
   */
  private double          effectiveMaxSpeed = -1;

  /**
   * The maximum distance, a player can move after three cycles, assuming he has
   * no current speed.
   */
  private double          moveDist          = -1;

  /**
   * A flag indicating the comparing attributes should be normalized within
   * their max and min-values. This is used within the compare function.
   */
  private final boolean         NORMALIZE         = true;

  /**
   * Reference to the info-object, which contains the PConf.
   */
  private PlayerParamInfo ppi               = null;

  /**
   * An object that contains the heterogeneous attributes of a player, e.g. his
   * dash-power. This constructor creates an object with default values (type
   * 0).
   */
  public PConf() {

  }

  /**
   * Sets the player-types maximum speed
   * 
   * @param psm -
   *          the new speed
   */
  public void setPlayerSpeedMax(final double psm) {

    this.PLAYER_SPEED_MAX = psm;
  }

  /**
   * Sets the player-types stamina increase-value.
   * 
   * @param sim -
   *          the new stamina increase-value
   */
  public void setStaminaInxMax(final double sim) {

    this.STAMINA_INC_MAX = sim;
  }

  /**
   * Sets the player-types decay.
   * 
   * @param pd -
   *          the new player-decay.
   */
  public void setPlayerDecay(final double pd) {

    this.PLAYER_DECAY = pd;
  }

  /**
   * Sets the player-types inertia.
   * 
   * @param im -
   *          the new inertia moment
   */
  public void setInertiaMoment(final double im) {

    this.INERTIA_MOMENT = im;
  }

  /**
   * Sets the player-types dash-power
   * 
   * @param dpr -
   *          the new dash-power
   */
  public void setDashPowerRate(final double dpr) {

    this.DASH_POWER_RATE = dpr;
  }

  /**
   * Sets the player-types size
   * 
   * @param ps -
   *          the new player size.
   */
  public void setPlayerSize(final double ps) {

    this.PLAYER_SIZE = ps;
  }

  /**
   * Sets the player-types kickable margin
   * 
   * @param km -
   *          the new kickable margin
   */
  public void setKickableMargin(final double km) {

    this.KICKABLE_MARGIN = km;
  }

  /**
   * Sets the random error for kicking
   * 
   * @param kr -
   *          the new error value
   */
  public void setKickRand(final double kr) {

    this.KICK_RAND = kr;
  }

  /**
   * Sets the player-types extra-stamina
   * 
   * @param es -
   *          the new extra stamina
   */
  public void setExtraStamina(final double es) {

    this.EXTRA_STAMINA = es;
  }

  /**
   * Sets the player-types maximal effort
   * 
   * @param em -
   *          the new maximal effort
   */
  public void setEffortMax(final double em) {

    this.EFFORT_MAX = em;
  }

  /**
   * Sets the player types minimal effort
   * 
   * @param em -
   *          the new minimal effort
   */
  public void setEffortMin(final double em) {

    this.EFFORT_MIN = em;
  }

  /**
   * Retrieves the player types id [0..6]
   * 
   * @return - the id
   */
  public int getId() {

    return this.id;
  }

  /**
   * Sets the player-types id
   * 
   * @param id -
   *          the id of the player-type, defined by the server
   */
  public void setId(final int id) {

    this.id = id;
  }

  /**
   * This method retrieves the maximum speed a player can reach when only
   * dashing
   * 
   * @return
   */
  public double getEffectiveMaxSpeed() {

    if (this.effectiveMaxSpeed < 0) {
      this.effectiveMaxSpeed = Math
          .min(
              (this.DASH_POWER_RATE * this.EFFORT_MAX * 100.0) / (1 - this.PLAYER_DECAY),
              this.PLAYER_SPEED_MAX);
    }
    return this.effectiveMaxSpeed;
  }

  /**
   * This method retrieves the maximum move distance after a fixed number of
   * cycles when not in motion
   * 
   * @return the distance moved
   */
  public double getMaxMoveDist() {

    if (this.moveDist < 0) {
      this.moveDist = this.DASH_POWER_RATE * this.EFFORT_MAX * 100.0 * (3 + 2 * this.PLAYER_DECAY + (this.PLAYER_DECAY * this.PLAYER_DECAY));
    }
    return this.moveDist;
  }

  /**
   * Lists the parameters in a readable way!
   */
  @Override
  public String toString() {

    final StringBuffer buff = new StringBuffer();
    buff.append(this.id);
    buff.append(' ');
    buff.append(this.PLAYER_ACCEL_MAX);
    buff.append(' ');
    buff.append(this.PLAYER_SPEED_MAX);
    buff.append(' ');
    buff.append(this.STAMINA_INC_MAX);
    buff.append(' ');
    buff.append(this.PLAYER_DECAY);
    buff.append(' ');
    buff.append(this.INERTIA_MOMENT);
    buff.append(' ');
    buff.append(this.DASH_POWER_RATE);
    buff.append(' ');
    buff.append(this.PLAYER_SIZE);
    buff.append(' ');
    buff.append(this.KICKABLE_MARGIN);
    buff.append(' ');
    buff.append(this.KICK_RAND);
    buff.append(' ');
    buff.append(this.EXTRA_STAMINA);
    buff.append(' ');
    buff.append(this.EFFORT_MAX);
    buff.append(' ');
    buff.append(this.EFFORT_MIN);
    return buff.toString();
  }

  /**
   * Clones this PConf object and returns it
   * 
   * @return a clone of this object!
   */
  public PConf cloned() {

    final PConf p = new PConf();
    p.DASH_POWER_RATE = this.DASH_POWER_RATE;
    p.EFFORT_MAX = this.EFFORT_MAX;
    p.EFFORT_MIN = this.EFFORT_MIN;
    p.EXTRA_STAMINA = this.EXTRA_STAMINA;
    p.INERTIA_MOMENT = this.INERTIA_MOMENT;
    p.KICK_RAND = this.KICK_RAND;
    p.KICKABLE_MARGIN = this.KICKABLE_MARGIN;
    p.PLAYER_ACCEL_MAX = this.PLAYER_ACCEL_MAX;
    p.PLAYER_DECAY = this.PLAYER_DECAY;
    p.PLAYER_SIZE = this.PLAYER_SIZE;
    p.PLAYER_SPEED_MAX = this.PLAYER_SPEED_MAX;
    p.STAMINA_INC_MAX = this.STAMINA_INC_MAX;
    p.id = this.id;
    return p;
  }

  /**
   * This method compares PConfs by an a-priori defined criteria. It is used to
   * sort PConfs by a given attribute, and then assign specific ones to certain
   * agents.
   * 
   * @param the
   *          PConf to compare to
   * @return the compare-value!
   */
  public int compareTo(final PConf other) {

    double thisWeight = (this.getEffectiveMaxSpeed() < 1.01) ? (0.1 / 1.2) : ((this
        .getEffectiveMaxSpeed() < 1.19) ? (0.5 / 1.2) : (this
        .getEffectiveMaxSpeed() / 1.2));

    double otherWeight = (other.getEffectiveMaxSpeed() < 1.01) ? (0.1 / 1.2) : ((other
        .getEffectiveMaxSpeed() < 1.19) ? (0.5 / 1.2) : (other
        .getEffectiveMaxSpeed() / 1.2));

    if (this.NORMALIZE) {
      switch (this.compareType) {
        case INERTIA:
          thisWeight /= this.INERTIA_MOMENT;
          otherWeight /= other.INERTIA_MOMENT;
          break;
        case KICKABLE_DISTANCE:
          thisWeight *= this.KICKABLE_MARGIN;
          otherWeight *= other.KICKABLE_MARGIN;
          break;
        case SPEED:
          thisWeight *= this.getMaxMoveDist();
          otherWeight *= other.getMaxMoveDist();
          break;
        case STAMINA:
          thisWeight *= (this.EXTRA_STAMINA + this.normalize(25, 45,
              this.STAMINA_INC_MAX)) / 2;
          otherWeight *= (other.EXTRA_STAMINA + other.normalize(25, 45,
              other.STAMINA_INC_MAX)) / 2;
          break;
        case STAMINA_AND_KICKABLE:
          thisWeight *= (((this.EXTRA_STAMINA + this.normalize(25, 45,
              this.STAMINA_INC_MAX)) / 2) + this.normalize(
              this.ppi.KICKABLE_MARGIN_MIN, this.ppi.KICKABLE_MARGIN_MAX,
              this.KICKABLE_MARGIN));
          otherWeight *= (((other.EXTRA_STAMINA + other.normalize(25, 45,
              other.STAMINA_INC_MAX)) / 2) + other.normalize(
              other.ppi.KICKABLE_MARGIN_MIN, other.ppi.KICKABLE_MARGIN_MAX,
              other.KICKABLE_MARGIN));
          break;
        case DASH_POWER:
          thisWeight *= this.DASH_POWER_RATE;
          otherWeight *= other.DASH_POWER_RATE;
          break;
        case DASH_POWER_AND_STAMINA:
          thisWeight *= (this.normalize(this.ppi.DASH_POWER_RATE_MIN,
              this.ppi.DASH_POWER_RATE_MAX, this.DASH_POWER_RATE) + ((this.EXTRA_STAMINA + this
              .normalize(25, 45, this.STAMINA_INC_MAX)) / 2));
          otherWeight *= (other.normalize(other.ppi.DASH_POWER_RATE_MIN,
              other.ppi.DASH_POWER_RATE_MAX, other.DASH_POWER_RATE) + (((other.EXTRA_STAMINA + other
              .normalize(25, 45, other.STAMINA_INC_MAX)) / 2)));
          break;
        case DASH_POWER_AND_DOUBLE_STAMINA:
          thisWeight *= (this.normalize(this.ppi.DASH_POWER_RATE_MIN,
              this.ppi.DASH_POWER_RATE_MAX, this.DASH_POWER_RATE) + this.EXTRA_STAMINA + this
              .normalize(25, 45, this.STAMINA_INC_MAX));
          otherWeight *= (other.normalize(other.ppi.DASH_POWER_RATE_MIN,
              other.ppi.DASH_POWER_RATE_MAX, other.DASH_POWER_RATE) + other.EXTRA_STAMINA + other
              .normalize(25, 45, other.STAMINA_INC_MAX));
          break;
        case DOUBLE_DASH_POWER_AND_STAMINA:
          thisWeight *= (this.normalize(this.ppi.DASH_POWER_RATE_MIN,
              this.ppi.DASH_POWER_RATE_MAX, this.DASH_POWER_RATE) * 2 + ((this.EXTRA_STAMINA + this
              .normalize(25, 45, this.STAMINA_INC_MAX)) / 2));
          otherWeight *= (other.normalize(other.ppi.DASH_POWER_RATE_MIN,
              other.ppi.DASH_POWER_RATE_MAX, other.DASH_POWER_RATE) * 2 + ((other.EXTRA_STAMINA + other
              .normalize(25, 45, other.STAMINA_INC_MAX)) / 2));
          break;
        case KICKABLE_AND_DASH_POWER_AND_STAMINA:
          thisWeight *= (this.normalize(this.ppi.KICKABLE_MARGIN_MIN,
              this.ppi.KICKABLE_MARGIN_MAX, this.KICKABLE_MARGIN) + this
              .normalize(this.ppi.DASH_POWER_RATE_MIN,
                  this.ppi.DASH_POWER_RATE_MAX, this.DASH_POWER_RATE) + ((this.EXTRA_STAMINA + this
              .normalize(25, 45, this.STAMINA_INC_MAX)) / 2));
          otherWeight *= (other.normalize(other.ppi.KICKABLE_MARGIN_MIN,
              other.ppi.KICKABLE_MARGIN_MAX, other.KICKABLE_MARGIN) + other
              .normalize(other.ppi.DASH_POWER_RATE_MIN,
                  other.ppi.DASH_POWER_RATE_MAX, other.DASH_POWER_RATE) + ((other.EXTRA_STAMINA + other
              .normalize(25, 45, other.STAMINA_INC_MAX)) / 2));
          break;
        case KICKABLE_AND_INERTIA:
          thisWeight *= (this.normalize(this.ppi.KICKABLE_MARGIN_MIN,
              this.ppi.KICKABLE_MARGIN_MAX, this.KICKABLE_MARGIN) + this
              .normalize(0.1, 0.2, 1 / this.INERTIA_MOMENT));
          otherWeight *= (other.normalize(other.ppi.KICKABLE_MARGIN_MIN,
              other.ppi.KICKABLE_MARGIN_MAX, other.KICKABLE_MARGIN) + other
              .normalize(0.1, 0.2, 1 / other.INERTIA_MOMENT));
          break;
      }
    }
    else {
      switch (this.compareType) {
        case INERTIA:
          thisWeight /= this.INERTIA_MOMENT;
          otherWeight /= other.INERTIA_MOMENT;
          break;
        case KICKABLE_DISTANCE:
          thisWeight *= this.KICKABLE_MARGIN;
          otherWeight *= other.KICKABLE_MARGIN;
          break;
        case SPEED:
          thisWeight *= this.getMaxMoveDist();
          otherWeight *= other.getMaxMoveDist();
          break;
        case STAMINA:
          thisWeight *= this.EXTRA_STAMINA * this.STAMINA_INC_MAX;
          otherWeight *= other.EXTRA_STAMINA * other.STAMINA_INC_MAX;
          break;
        case STAMINA_AND_KICKABLE:
          thisWeight *= this.EXTRA_STAMINA * this.STAMINA_INC_MAX * this.KICKABLE_MARGIN;
          otherWeight *= other.EXTRA_STAMINA * other.STAMINA_INC_MAX * other.KICKABLE_MARGIN;
          break;
        case DASH_POWER:
          thisWeight *= this.DASH_POWER_RATE;
          otherWeight *= other.DASH_POWER_RATE;
          break;
        case DASH_POWER_AND_STAMINA:
          thisWeight *= this.DASH_POWER_RATE * this.STAMINA_INC_MAX * this.EXTRA_STAMINA;
          otherWeight *= other.DASH_POWER_RATE * other.STAMINA_INC_MAX * other.EXTRA_STAMINA;
          break;
        case DASH_POWER_AND_DOUBLE_STAMINA:
          thisWeight *= this.DASH_POWER_RATE * Math.pow(
              this.STAMINA_INC_MAX * this.EXTRA_STAMINA, 2);
          otherWeight *= other.DASH_POWER_RATE * Math.pow(
              other.STAMINA_INC_MAX * other.EXTRA_STAMINA, 2);
          break;
        case DOUBLE_DASH_POWER_AND_STAMINA:
          thisWeight *= Math.pow(this.DASH_POWER_RATE, 2) * this.STAMINA_INC_MAX * this.EXTRA_STAMINA;
          otherWeight *= Math.pow(other.DASH_POWER_RATE, 2) * other.STAMINA_INC_MAX * other.EXTRA_STAMINA;
          break;
        case KICKABLE_AND_DASH_POWER_AND_STAMINA:
          thisWeight *= this.KICKABLE_MARGIN * this.DASH_POWER_RATE * this.STAMINA_INC_MAX * this.EXTRA_STAMINA;
          otherWeight *= other.KICKABLE_MARGIN * other.DASH_POWER_RATE * other.STAMINA_INC_MAX * other.EXTRA_STAMINA;
          break;
        case KICKABLE_AND_INERTIA:
          thisWeight *= this.KICKABLE_MARGIN / this.INERTIA_MOMENT;
          otherWeight *= other.KICKABLE_MARGIN / other.INERTIA_MOMENT;
          break;
      }
    }

    if (thisWeight > otherWeight) {
      return -1;
    }
    else if (thisWeight < otherWeight) {
      return 1;
    }
    return 0;
  }

  /**
   * Normalizes a value to the interval [0,1], using the min, the max and the
   * current value of an attribute.
   * 
   * @param min -
   *          the minimum value
   * @param max -
   *          the maximum value
   * @param value -
   *          the value that should be normalized
   * @return - the normalized value
   */
  private double normalize(final double min,
      final double max,
      final double value) {

    return (value - min) * 100 / (max - min);
  }

  /**
   * Retrieves the actual type by the PConf is currently compared, when calling
   * the <code>compareTo</code>-method.
   * 
   * @return - the active compare-type
   */
  public COMPARE_TYPE getCompareType() {

    return this.compareType;
  }

  /**
   * Sets the compare type to the given one.
   * 
   * @param compareType -
   *          the new compare-type.
   */
  public void setCompareType(final COMPARE_TYPE compareType) {

    this.compareType = compareType;
  }

  /**
   * Sets the player-param-info, which is the container for the PConf.
   * 
   * @param ppi -
   *          the player-param-info, received from the server and created by the
   *          parser.
   */
  public void setPlayerParamInfo(final PlayerParamInfo ppi) {

    this.ppi = ppi;
  }

}
